// @flow
import { Component, createElement, createRef } from 'react'
import PropTypes from 'prop-types'
import invariant from 'invariant'
import get from 'lodash/get'
import createConnectedFields from './ConnectedFields'
import shallowCompare from './util/shallowCompare'
import plain from './structure/plain'
import prefixName from './util/prefixName'
import { withReduxForm } from './ReduxFormContext'
import type { Structure, ReactContext } from './types'
import type { Props as PropsWithoutContext, WarnAndValidateProp } from './FieldsProps.types'
import validateComponentProp from './util/validateComponentProp'

type Props = ReactContext & PropsWithoutContext

const validateNameProp = prop => {
  if (!prop) {
    return new Error('No "names" prop was specified <Fields/>')
  }
  if (!Array.isArray(prop) && !prop._isFieldArray) {
    return new Error(
      'Invalid prop "names" supplied to <Fields/>. Must be either an array of strings or the fields array generated by FieldArray.'
    )
  }
}

const warnAndValidatePropType = PropTypes.oneOfType([
  PropTypes.func,
  PropTypes.arrayOf(PropTypes.func),
  PropTypes.objectOf(PropTypes.oneOfType([PropTypes.func, PropTypes.arrayOf(PropTypes.func)]))
])
const fieldsPropTypes = {
  component: validateComponentProp,
  format: PropTypes.func,
  parse: PropTypes.func,
  props: PropTypes.object,
  forwardRef: PropTypes.bool,
  validate: warnAndValidatePropType,
  warn: warnAndValidatePropType
}

const getFieldWarnAndValidate = (prop?: WarnAndValidateProp, name) =>
  Array.isArray(prop) || typeof prop === 'function' ? prop : get(prop, name, undefined)

export default function createFields(structure: Structure<any, any>) {
  const ConnectedFields = createConnectedFields(structure)

  class Fields extends Component<Props> {
    connected = createRef<ConnectedFields>()

    constructor(props: Props) {
      super((props: Props))
      if (!props._reduxForm) {
        throw new Error('Fields must be inside a component decorated with reduxForm()')
      }
      const error = validateNameProp(props.names)
      if (error) {
        throw error
      }
    }

    shouldComponentUpdate(nextProps: Props) {
      return shallowCompare(this, nextProps)
    }

    componentDidMount() {
      this.registerFields(this.props.names)
    }

    UNSAFE_componentWillReceiveProps(nextProps: Props) {
      if (!plain.deepEqual(this.props.names, nextProps.names)) {
        const { props } = this
        const { unregister } = props._reduxForm
        // unregister old name
        this.props.names.forEach(name => unregister(prefixName(props, name)))
        // register new name
        this.registerFields(nextProps.names)
      }
    }

    componentWillUnmount() {
      const { props } = this
      const { unregister } = props._reduxForm
      this.props.names.forEach(name => unregister(prefixName(props, name)))
    }

    registerFields(names: string[]) {
      const { props } = this
      const {
        _reduxForm: { register }
      } = props
      names.forEach(name =>
        register(
          prefixName(props, name),
          'Field',
          () => getFieldWarnAndValidate(this.props.validate, name),
          () => getFieldWarnAndValidate(this.props.warn, name)
        )
      )
    }

    getRenderedComponent() {
      invariant(
        this.props.forwardRef,
        'If you want to access getRenderedComponent(), ' +
          'you must specify a forwardRef prop to Fields'
      )
      return this.connected.current ? this.connected.current.getRenderedComponent() : null
    }

    get names(): string[] {
      const { props } = this
      return this.props.names.map(name => prefixName(props, name))
    }

    get dirty(): boolean {
      return this.connected.current ? this.connected.current.isDirty() : false
    }

    get pristine(): boolean {
      return !this.dirty
    }

    get values(): Object {
      return this.connected.current ? this.connected.current.getValues() : {}
    }

    render() {
      const { props } = this
      return createElement(ConnectedFields, {
        ...this.props,
        names: this.props.names.map(name => prefixName(props, name)),
        ref: this.connected
      })
    }
  }

  Fields.propTypes = {
    names: (props, propName) => validateNameProp(props[propName]),
    ...fieldsPropTypes
  }

  return withReduxForm(Fields)
}
